from collections import defaultdict

from django.db import models
from django.contrib.auth.models import User
from datetime import datetime


class Category(models.Model):
    """Product category."""
    name = models.CharField(max_length=255)
    position = models.IntegerField(unique=True)

    def __unicode__(self):
        return self.name

    class Meta:
        ordering = ['position']
        verbose_name_plural = 'categories'


class Product(models.Model):
    """Product sold in the web shop."""
    name = models.CharField(max_length=255)
    description = models.TextField()
    price = models.DecimalField(max_digits=7, decimal_places=2, default=0)
    stock = models.IntegerField(default=0)
    category = models.ForeignKey(Category)

    def __unicode__(self):
        return self.name


class Rating(models.Model):
    """User's rating of a product."""
    user = models.ForeignKey(User)
    product =  models.ForeignKey(Product)
    stars = models.IntegerField()

    class Meta:
        unique_together = (('user', 'product'), )

    def __unicode__(self):
        return u'%s rated %s as %s.' % (self.user.username, self.product.name,
                                        self.stars)


class Address(models.Model):
    """Street address."""
    name = models.CharField(max_length=255)
    street_address = models.CharField(max_length=255)
    postal_code = models.CharField(max_length=10)
    city = models.CharField(max_length=50)
    country = models.CharField(max_length=50)

    class Meta:
        verbose_name_plural = 'addresses'

    def copy(self):
        return Address(name=self.name, street_address=self.street_address,
                       postal_code=self.postal_code, city=self.city,
                       country=self.country)

    def __unicode__(self):
        return ', '.join((self.name, self.street_address,
                          self.postal_code + ' ' + self.city, self.country))



class UserProfile(models.Model):
    user = models.ForeignKey(User, unique=True)
    shipping_address = models.OneToOneField(Address)
    phonenumber = models.CharField(max_length=20)

    def __unicode__(self):
        return u'%s\'s profile' % (self.user.username)


class Order(models.Model):
    """A collection of `Products` the user has ordered from the shop."""

    STATUS = (
        ('W', 'Waiting for payment'),
        ('P', 'Paid. Waiting for delivery.'),
        ('D', 'Delivered')
    )

    user = models.ForeignKey(User)
    products = models.ManyToManyField(Product, through='OrderProduct')
    status = models.CharField(max_length=1, choices=STATUS)
    created = models.DateTimeField(auto_now_add=True, editable=False)
    shipped = models.DateTimeField(editable=False, null=True)
    shipping_address = models.OneToOneField(Address, null=True)
    phonenumber = models.CharField(max_length=20)

    def __init__(self, *args, **kwargs):
        super(Order, self).__init__(*args, **kwargs)
        self._items = None

    def save(self, *args, **kwargs):
        try:
            old = Order.objects.get(id=self.id)
            if old.status != 'D' and self.status == 'D':
                self.shipped = datetime.now()
        except Order.DoesNotExist:
            pass

        super(Order, self).save(*args, **kwargs)

    @property
    def items(self):
        """The contents of the order as a list of `Items`."""
        if self._items is None:
            order_products = OrderProduct.objects.filter(order=self).all()
            self._items = [Item(product=order_product.product,
                                quantity=order_product.quantity)
                           for order_product in order_products]
        return self._items

    @property
    def total_price(self):
        return sum(item.total_price for item in self.items)

    def __unicode__(self):
        return u'Order #%s' % self.id


class OrderProduct(models.Model):
    """Association class between `Order` and `Product`."""
    order = models.ForeignKey(Order)
    product = models.ForeignKey(Product)
    quantity = models.PositiveIntegerField()
    unit_price = models.DecimalField(max_digits=7, decimal_places=2)

    class Meta:
        unique_together = (('order', 'product'), )

    @property
    def total_price(self):
        """Total price of this kind of products in the order."""
        return self.quantity * self.unit_price

    def __unicode__(self):
        return self.product.name


class Cart(object):
    """Represents a shopping cart in session.

    The shopping cart contains `Products` and their quantities.

    """
    SESSION_KEY = 'cart'

    def __init__(self, request):
        self._request = request

        if self.SESSION_KEY not in self._request.session:
            self._request.session[self.SESSION_KEY] = defaultdict(int)

        self._contents = self._request.session[self.SESSION_KEY]
        self._items = None

    def _cart_modified(self):
        """Sets this cart as modified during the request."""
        self._request.session.modified = True
        self._items = None

    def add(self, product):
        """Add the given product to the shopping cart.

        :param product: The `Product` which is added to the shopping
          cart.  If there already is such product in the cart, its quantity
          is updated.

        """
        self._contents[product.id] += 1
        self._cart_modified()

    def remove(self, product):
        """Remove the given product from the shopping cart.

        :param product: The `Product` which is removed from the shopping cart.
          If there are more than one such product in the cart, all of them are
          removed from the cart.  If there is no such product in the cart, a
          `KeyError` is raised.

        """
        del self._contents[product.id]
        self._cart_modified()

    def clear(self):
        """Remove all products from the shopping cart."""
        self._contents.clear()
        self._cart_modified()

    def update(self, product, quantity):
        """Update the quantity of the given product in the shopping cart.

        :param product: `Product` whose quantity is updated.  The product does
          not need to be in the cart.

        :param quantity: The new quantity for the product in the cart.  If
          the new quantity is less than one, the product is removed from the
          cart.  Quantity is expected to be an integer.

        """
        if quantity > 0:
            self._contents[product.id] = quantity
            self._cart_modified()
        else:
            self.remove(product)

    @property
    def items(self):
        """The contents of the shopping cart as a list of `Items`."""
        if self._items is None:
            products = Product.objects.filter(id__in=self._contents.keys()).all()
            self._items = [Item(product=product,
                                quantity=self._contents[product.id])
                           for product in products]
        return self._items

    @property
    def total_price(self):
        """Total price of all products in the shopping cart."""
        return sum(item.total_price for item in self.items)

    def __len__(self):
        """Return the total number of products in the shopping cart."""
        return sum(quantity for quantity in self._contents.itervalues())


class Item(object):
    """`Item` represents an item in a shopping cart.

    The item consists of a `Product` and its quantity in the cart.

    """
    def __init__(self, product, quantity):
        self.product = product
        self.quantity = quantity

    @property
    def total_price(self):
        """Total price of this kind of products in the cart."""
        return self.quantity * self.product.price

